RecyclerView 中的秘密探索,起飞 Fling ~
本文作者
作者:唐子玄
链接:
https://juejin.cn/post/6960457567680069645
本文由作者授权发布。
之前介绍过手指滑动过程中,列表的滚动是如何实现的:
那脱手之后,列表仍会滚动一段距离,即 fling,这又是如何实现的?走查源码一探究竟。
写一个简单的列表 demo,用 Profiler 记录脱手滚动过程中完整的调用链(关于如何用 Profiler 找到源码执行的关键路径可以点击RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势):
https://juejin.cn/post/6958962329220284453
脱手后会触发RecyclerView.fling(),它的调用点在RecyclerView.onTouchEvent()中:
public class RecyclerView {
@Override
public boolean onTouchEvent(MotionEvent e) {
...
final MotionEvent vtev = MotionEvent.obtain(e);
switch (action) {
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
// 计算速率
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
// 触发 fling
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetScroll();
} break;
}
...
return true;
}
}
ACTION_UP事件发生时,将VelocityTracker计算得出的速率传入RecyclerView.fling()来触发fling:
public class RecyclerView {
public boolean fling(int velocityX, int velocityY) {
...
// 若滚动速率小于阈值 则直接 return 不触发 fling
if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
velocityX = 0;
}
if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
velocityY = 0;
}
if (velocityX == 0 && velocityY == 0) {
return false;
}
// 让父控件提前消费 fling
if (!dispatchNestedPreFling(velocityX, velocityY)) {
...
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
// 委托给 ViewFlinger 触发 fling
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
return false;
}
}
RecyclerView 在触发 fling 之前,处理了嵌套滚动逻辑,即让父控件提前消费 fling,若父控件未消费,则将 fling 委托给ViewFlinger:
public class RecyclerView {
class ViewFlinger implements Runnable {
OverScroller mOverScroller;
public void fling(int velocityX, int velocityY) {
...
// 计算 fling 相关的数值
mOverScroller.fling(0, 0, velocityX, velocityY,Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
// 将 ViewFlinger 作为动画抛出
postOnAnimation();
}
void postOnAnimation() {
...
// 将 ViewFlinger 作为动画抛出
internalPostOnAnimation();
}
private void internalPostOnAnimation() {
removeCallbacks(this);
// 将 ViewFlinger post 到主线程消息队列
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
}
}
ViewFlinger是一个 Runnable。列表脱手滚动逻辑也被包装在这个 Runnable 中并 post 到主线消息队列中:
public class ViewCompat {
public static void postOnAnimation(@NonNull View view, Runnable action) {
if (Build.VERSION.SDK_INT >= 16) {
// 将抛消息委托给 view
view.postOnAnimation(action);
} else {
view.postDelayed(action, ValueAnimator.getFrameDelay());
}
}
}
public class View {
public void postOnAnimation(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// 将抛消息委托给 Choreographer 并将消息类型指定为 CALLBACK_ANIMATION
attachInfo.mViewRootImpl.mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, action, null);
} else {
getRunQueue().post(action);
}
}
}
Choreographer是用来将上层绘制任务和底层垂直同步信号进行协调的一个类:
public final class Choreographer {
// 输入任务
public static final int CALLBACK_INPUT = 0;
// 动画任务
public static final int CALLBACK_ANIMATION = 1;
// view树遍历任务
public static final int CALLBACK_TRAVERSAL = 2;
// COMMIT任务
public static final int CALLBACK_COMMIT = 3;
// 暂存任务的链式数组
private final CallbackQueue[] mCallbackQueues;
// 主线程消息处理器
private final FrameHandler mHandler;
// 抛绘制任务
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
// 延迟抛绘制任务
public void postCallbackDelayed(int callbackType,Runnable action, Object token, long delayMillis) {
...
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
// 抛绘制任务的具体实现
private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
// 1. 将绘制任务根据类型暂存在链式结构中
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
// 2. 订阅下一个垂直同步信号
if (dueTime <= now) {
// 立刻订阅下一个垂直同步信号
scheduleFrameLocked(now);
} else {
// 在未来的某个时间点订阅垂直同步信号
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
// 主线程消息处理器
private final class FrameHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
...
case MSG_DO_SCHEDULE_CALLBACK:
// 在未来时间点订阅垂直同步信号
doScheduleCallback(msg.arg1);
break;
}
}
}
}
Choreographer内部维护了一个链式数组结构CallbackQueue[],数组中存放四种类型的任务,分别是输入任务、动画任务、view树遍历任务、COMMIT任务。
这些任务在下一个垂直同步信号到来之时,会被取出执行。
public final class Choreographer {
// 垂直同步信号接收器
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
// 垂直同步信号到来
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
...
// 发送异步消息到主线程,执行当前的 Runnable,即 doFrame()
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
// 绘制一帧的内容
doFrame(mTimestampNanos, mFrame);
}
}
}
其中doFrome()表示绘制垂直信号到来的当前帧:
public final class Choreographer {
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
...
try {
// 处理这一帧的输入事件
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
// 处理这一帧的动画
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 处理这一帧的 View 树遍历
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
// 所有绘制任务结束后执行 COMMIT 任务
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
}
}
}
绘制当前帧会按序处理之前暂存的四种任务。更详细的Choreographer解析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?
https://juejin.cn/post/6864365886837686285/
至此可以得出结论:
RecyclerView 的脱手滚动不是立刻执行的,触发脱手滚动时,一个叫ViewFlinger的Runnable会被抛到Choreographer中,它被包装成一个动画任务。等待下一个垂直同步信号到来时,这个任务就在主线程被执行。
这个被抛到主线程执行的任务就在ViewFlinger.run()中:
public class RecyclerView {
class ViewFlinger implements Runnable {
@Override
public void run() {
...
final OverScroller scroller = mOverScroller;
// OverScroller 计算滚动位置 返回 true 表示滚动还未完成
if (scroller.computeScrollOffset()) {
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
int unconsumedX = x - mLastFlingX;
int unconsumedY = y - mLastFlingY;
mLastFlingX = x;
mLastFlingY = y;
int consumedX = 0;
int consumedY = 0;
// 回调嵌套滚动,以让 RecyclerView 的父控件优先消费滚动距离
if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null, TYPE_NON_TOUCH)) {
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
}
...
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 列表自己滚动一小段,将嵌套滚动的剩余值给 RecyclerView 消费
scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX -= consumedX;
unconsumedY -= consumedY;
...
}
...
// 判断当前滚动位置和目标滚动位置是否相等
boolean scrollerFinishedX = scroller.getCurrX() == scroller.getFinalX();
boolean scrollerFinishedY = scroller.getCurrY() == scroller.getFinalY();
// 通过 OverScroller 判断滚动是否结束
final boolean doneScrolling = scroller.isFinished()
|| ((scrollerFinishedX || unconsumedX != 0)
&& (scrollerFinishedY || unconsumedY != 0));
SmoothScroller smoothScroller = mLayout.mSmoothScroller;
boolean smoothScrollerPending =
smoothScroller != null && smoothScroller.isPendingInitialRun();
if (!smoothScrollerPending && doneScrolling) {
...
} else {
// 如果滚动还未结束,则继续将自己(ViewFlinger)抛到主线程执行
postOnAnimation();
if (mGapWorker != null) {
mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY);
}
}
}
...
}
}
}
RecyclerView 的脱手滚动是一小段一小段进行的。每一小段的滚动通过RecyclerView.scrollStep()落实。在RecyclerView 的滚动是怎么实现的?| 解锁阅读源码新姿势中已经介绍过,它会根据滚动距离在滚动方向上填充额外的表项,然后将所有表项向滚动的反方向平移相同距离,以实现滚动。
https://juejin.cn/post/6958962329220284453
滚完了一小段,是否还要滚下一段?这是由OverScroller说了算的,如果当前滚动位置和目标滚动位置不相等,则表示滚动还未结束,此时会再次执行postOnAnimation()将ViewFlinger这个 Runnable 抛到主线程执行。如此往复列表就自己滚起来了。
OverScroller用于存储并计算所有和滚动相关的数值:
public class OverScroller {
// 横向 Scroller
private final SplineOverScroller mScrollerX;
// 纵向 Scroller
private final SplineOverScroller mScrollerY;
static class SplineOverScroller {
// 初始滚动位置
private int mStart;
// 当前滚动位置
private int mCurrentPosition;
// 目标滚动位置
private int mFinal;
// 滚动速率
private int mVelocity;
// 滚动开始时间
private long mStartTime;
// 滚动时长
private int mDuration;
...
}
}
在每个ViewFlinger.run()被执行的开头,通过OverScroll.computeScrollOffset()触发更新这次要滚的那一小段位移值:
public class OverScroller {
public boolean computeScrollOffset() {
if (isFinished()) {
return false;
}
switch (mMode) {
// 跟手滚动时位移计算算法
case SCROLL_MODE:
long time = AnimationUtils.currentAnimationTimeMillis();
final long elapsedTime = time - mScrollerX.mStartTime;
final int duration = mScrollerX.mDuration;
if (elapsedTime < duration) {
final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
mScrollerX.updateScroll(q);
mScrollerY.updateScroll(q);
} else {
abortAnimation();
}
break;
// 脱手滚动时位移计算算法
case FLING_MODE:
// 横向
if (!mScrollerX.mFinished) {
if (!mScrollerX.update()) {
if (!mScrollerX.continueWhenFinished()) {
mScrollerX.finish();
}
}
}
// 纵向
if (!mScrollerY.mFinished) {
if (!mScrollerY.update()) {
if (!mScrollerY.continueWhenFinished()) {
mScrollerY.finish();
}
}
}
break;
}
return true;
}
}
对于 fling 来说,计算滚动位移的算法在update()方法中:
public class OverScroller {
static class SplineOverScroller {
// 根据一定算法更新当前位置
boolean update() {
final long time = AnimationUtils.currentAnimationTimeMillis();
final long currentTime = time - mStartTime;
if (currentTime == 0) {
return mDuration > 0;
}
if (currentTime > mDuration) {
return false;
}
double distance = 0.0;
// 根据不同的状态选择不同算法计算滚动速率及当前滚动位置
switch (mState) {
case SPLINE: {
final float t = (float) currentTime / mSplineDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
distance = distanceCoef * mSplineDistance;
mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
break;
}
case BALLISTIC: {
final float t = currentTime / 1000.0f;
mCurrVelocity = mVelocity + mDeceleration * t;
distance = mVelocity * t + mDeceleration * t * t / 2.0f;
break;
}
case CUBIC: {
final float t = (float) (currentTime) / mDuration;
final float t2 = t * t;
final float sign = Math.signum(mVelocity);
distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
break;
}
}
//更新当前滚动位置
mCurrentPosition = mStart + (int) Math.round(distance);
return true;
}
}
}
算法很复杂,看也看不懂(猜测手机厂商会优化这块以实现更顺滑的手感),不过也不影响今天探讨的主题。待以后有需求时再深挖。
算法的终点就是更新当前滚动位置mCurrentPosition的值。
总结
对于RecyclerView,不管是跟手还是脱手滚动,最终滚动的落实都是通过调用View.offsetTopAndBottom()向滚动的反方向平移表项实现的。
OverScroller不仅存储并计算了和滚动相关的所有数值。RecyclerView 还借助于它来控制滚动是否要继续。
RecyclerView 的脱手滚动(fling)是一段一段进行的,每一小段的滚动都被包裹在一个叫ViewFlinger的 Runnable 中。它会被抛到Choreographer中,作为动画任务暂存起来。待一下个垂直同步信号到来之时,被抛到主线程的消息队列中执行。
只要OverScroller判断滚动尚未结束,ViewFlinger会重复上述过程,即再一次把自己抛到主线程中执行。如此往复,列表就脱手滚了起来。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!